/*******************************************************************************
 * Copyright 2007, 2009 InnovaSoft SA http://www.innovasoft.biz/
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *******************************************************************************/

package biz.innovasoft.ndb.util;

import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.Pointer;
import org.apache.lucene.document.DateTools;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.NumberTools;

import biz.innovasoft.ndb.index.DateResolution;
import biz.innovasoft.ndb.index.IndexEntry;
import biz.innovasoft.ndb.index.IndexException;
import biz.innovasoft.ndb.index.IndexManager;
import biz.innovasoft.ndb.index.Null;
import biz.innovasoft.ndb.query.Expression;
import biz.innovasoft.ndb.query.MultinaryExpression;
import biz.innovasoft.ndb.query.Operator;

public class IndexUtils {

	private static DecimalFormat numberFormatter = new DecimalFormat(
			"000000000000");

	private static Class<?>[] primitiveClasses = { String.class, Number.class,
			Boolean.class, Character.class, Byte.class, Integer.class,
			Float.class, Double.class, Long.class, boolean.class, char.class,
			short.class, int.class, long.class, float.class, double.class,
			byte.class, Date.class };

	public static String format(int number) {
		return numberFormatter.format(number);
	}

	/**
	 * Builds a document to store in the index.
	 * 
	 * @param manager the index definition.
	 * @param object the object data to build the document.
	 * @return the document to be stored in the index.
	 * @throws IndexException
	 */
	public static Document buildDocument(IndexManager manager, Object object)
			throws IndexException {
		Document document = new Document();
		List<Field> fields = getDocumentFields(manager.getIndexEntries(),
				object);
		for (Field field : fields) {
			document.add(field);
		}
		return document;
	}

	/**
	 * Returns a list of Lucene Fields with the super classes of the object.
	 * 
	 */
	public static List<Field> getClassFields(Object object) {
		List<Field> lfields = new ArrayList<Field>();

		Field lfield = new Field("class", object.getClass().getCanonicalName(),
				Field.Store.YES, Field.Index.NOT_ANALYZED);
		lfields.add(lfield);

		List<String> sclasses = getSuperclassesFor(object.getClass());
		if (sclasses.size() != 0) {
			for (String clazz : sclasses) {
				lfield = new Field("class", clazz, Field.Store.YES,
						Field.Index.NOT_ANALYZED);
				lfields.add(lfield);
			}
		}

		return lfields;
	}

	/**
	 * Returns a list of Lucene Fields 
	 */
	private static List<Field> buildDeclaredFields(IndexEntry entry,
			Class<?> clazz, Object object) throws IndexException {
		List<Field> docFields = new ArrayList<Field>();
		//java.lang.reflect.Field[] declaredFields = clazz.getDeclaredFields();
		java.lang.reflect.Field[] declaredFields = clazz.getFields();
		for (java.lang.reflect.Field field : declaredFields) {
			String fieldName = entry.getSearchPath() + "." + field.getName();
			Object value = null;
			try {
				field.setAccessible(true);
				value = field.get(object);
				if (value == null && !entry.isAllowNulls()) {
					throw new IndexException(
							"Not null value accepted for the index entry: "
									+ entry.getIndexPath());
				}
			} catch (IllegalAccessException e) {
				throw new IndexException("Error accesing to the field: "
						+ field.getName(), e);
			}
			Field df = buildField(fieldName, value, entry.getResolution());
			docFields.add(df);
		}
		return docFields;
	}
	
	/**
	 * Returns the root class in hierachy. 
	 */
	public static String getRootClassName (Class<?> clazz) {
		Class<?> superclass = clazz.getSuperclass();
		if (superclass.equals(Object.class)) {
			return clazz.getCanonicalName();
		}
		return getRootClassName(superclass);
	}
	
	/**
	 * Returns the root class in hierachy. 
	 */
	public static Class<?> getRootClass (Class<?> clazz) {
		Class<?> superclass = clazz.getSuperclass();
		if (superclass.equals(Object.class)) {
			return clazz;
		}
		return getRootClass(superclass);
	}
	

	private static List<Field> getDocumentFields(IndexEntry[] entries,
			Object object) throws IndexException {
		List<Field> docFields = new ArrayList<Field>();
		for (IndexEntry entry : entries) {
			List<Field> pathFields = iterateEntry(entry, object);
			docFields.addAll(pathFields);
		}
		return docFields;
	}

	private static List<Field> iterateEntry(IndexEntry entry, Object object)
			throws IndexException {
		List<Field> docFields = new ArrayList<Field>();
		JXPathContext context = JXPathContext.newContext(object);
		Iterator<?> ptr = context.iteratePointers(entry.getIndexPath());
		while (ptr.hasNext()) {
			Pointer optr = (Pointer) ptr.next();
			Object value = optr.getValue();
			if (value == null && !entry.isAllowNulls()) {
				throw new IndexException(
						"Not null value accepted for the index entry: "
								+ entry.getIndexPath());
			}
			if (value == null || isPrimitiveClass(value.getClass())) {
				Field f = buildField(entry.getSearchPath(), value, entry.getResolution());
				docFields.add(f);
			} else {
				List<Field> fields = buildDeclaredFields(entry, value
						.getClass(), value);
				docFields.addAll(fields);
			}
		}
		return docFields;
	}

	private static boolean isPrimitiveClass(Class<?> clazz) {
		for (Class<?> cls : primitiveClasses) {
			if (cls.equals(clazz)) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Builds the Lucene field for the object.
	 *  
	 * @param fieldName the name of the index attribute.
	 * @param object the value being stored in the index.
	 * @return a Lucene field to be used in the document.
	 */
	private static Field buildField(String fieldName, Object object, DateResolution resolution) {

		if (object == null) {
			return new Field(fieldName, Null.NULL.toString(), Field.Store.YES,
					Field.Index.NOT_ANALYZED);
		}
		Class<?> type = object.getClass();
		if (type.getName().equals("java.lang.String")) {
			return new Field(fieldName, object.toString(), Field.Store.YES,
					Field.Index.NOT_ANALYZED);
		}
		if (type.getName().equals("java.lang.Integer")
				|| type.getName().equals("int")) {
			return new Field(fieldName, NumberTools.longToString((Integer) object),
					Field.Store.YES, Field.Index.NOT_ANALYZED);
		}
		if (type.equals(Long.class) || type.equals(long.class)) {
			return new Field(fieldName, NumberTools.longToString((Long) object),
					Field.Store.YES, Field.Index.NOT_ANALYZED);
		}
		if (type.equals(Date.class)) {
			if (resolution == null) {
				return new Field(fieldName, DateTools.dateToString((Date) object, DateTools.Resolution.DAY),
					Field.Store.YES, Field.Index.NOT_ANALYZED);
			}
			return new Field(fieldName, DateTools.dateToString((Date) object, resolution.toLuceneResolution()),
					Field.Store.YES, Field.Index.NOT_ANALYZED);
		}
		return new Field(fieldName, object.toString(), Field.Store.YES,
				Field.Index.NOT_ANALYZED);
	}

	private static List<String> getSuperclassesFor(Class<?> clazz) {
		List<String> superclasses = new ArrayList<String>();
		Class<?> superclass = clazz.getSuperclass();
		do {
			if (superclass.equals(Object.class)) {
				break;
			}
			superclasses.add(superclass.getCanonicalName());
			superclass = superclass.getSuperclass();
		} while (true);
		return superclasses;
	}

	private static List<String> getExpressionAttribute(Expression expr) {
		List<String> attributes = new ArrayList<String>();

		if (expr instanceof MultinaryExpression) {
			return getExpressionAttributes(((MultinaryExpression) expr).expressions);
		} else {
			if (expr.operator.equals(Operator.AND)
					|| expr.operator.equals(Operator.OR)) {
				attributes.addAll(getExpressionAttribute(expr.right));
				if (expr.left != null) {
					attributes.addAll(getExpressionAttribute(expr.left));
				}

			} else {
				attributes.add((String) expr.attr);
			}
		}
		return attributes;
	}

	public static List<String> getExpressionAttributes(
			List<Expression> expressions) {
		List<String> attributes = new ArrayList<String>();
		for (Expression expr : expressions) {
			attributes.addAll(getExpressionAttribute(expr));
		}
		return attributes;
	}
	
	public static String[] getExpressionAttributesAsArray (List<Expression> expressions) {
		List<String> attr = getExpressionAttributes (expressions);
		return (String[])attr.toArray(new String[0]);
	}

	@SuppressWarnings("unused")
	private static List<Object> getObjectValues(String attr, Object object) {
		List<Object> values = new ArrayList<Object>();
		JXPathContext context = JXPathContext.newContext(object);
		Iterator<?> ptr = context.iteratePointers(attr);
		while (ptr.hasNext()) {
			Pointer optr = (Pointer) ptr.next();
			if (optr == null) {
				break;
			}
			Object value = optr.getValue();
			if (value != null) {
				values.add(value);
			} else {
				values.add(Null.NULL);
			}

		}
		return values;
	}

}
